Go sync 包近两年发展综述

Go 语言的 sync 包是其并发编程模型的基石,提供了实现同步和并发控制的关键原语。在过去的两年里(大约从 Go 1.19 到 1.25),sync 包及其子包 sync/atomic 经历了一系列重要的演进。这些变化不仅包括新功能的增加,还涉及性能优化、内部实现的重构以及开发者体验的显著提升。

本文基于 Go 语言官方仓库的 Git 提交历史,对 sync 包近两年的主要变化进行总结。

1. 新增 API 与功能增强

为了简化常见的并发模式,sync 包引入了一些备受期待的新 API。

sync.WaitGroup.Go

Go 1.25 引入了 WaitGroup.Go 方法,极大地简化了在 WaitGroup 中启动 goroutine 的代码。

旧模式:

1
2
3
4
5
wg.Add(1)
go func() {
defer wg.Done()
// ... do work ...
}()

新模式:

1
2
3
wg.Go(func() {
// ... do work ...
})

这个辅助方法不仅减少了样板代码,还通过内置的 defer wg.Done() 调用避免了忘记调用 Done() 的常见错误。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
urls := []string{
"http://example.com",
"http://example.org",
"http://example.net",
}
for _, url := range urls {
// 使用 wg.Go 启动 goroutine
wg.Go(func() {
// 模拟抓取 URL
fmt.Printf("Fetching %s\n", url)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Fetched %s\n", url)
})
}
// 等待所有 wg.Go 启动的 goroutine 完成
wg.Wait()
fmt.Println("All fetches completed.")
}

sync.Map.Clearsync.Map.Swap

sync.Map 也获得了一些实用的新方法:

  • Clear(): 用于一次性删除 Map 中的所有键值对,提供了一种高效清空 Map 的标准方式 (Go 1.23.0)。
  • Swap(): 原子性地交换一个键的新旧值 (Go 1.20)。
  • CompareAndSwap() / CompareAndDelete(): 提供了更精细的原子操作,允许用户基于旧值进行条件交换或删除 (Go 1.20)。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 存储键值对
m.Store("config", "v1")
m.Store("feature_flag", "off")
// Swap: 原子性地将 "config" 的值更新为 "v2",并返回旧值
oldValue, loaded := m.Swap("config", "v2")
if loaded {
fmt.Printf("Swapped 'config'. Old value was: %s\n", oldValue)
}
// 打印当前值
currentValue, _ := m.Load("config")
fmt.Printf("Current value of 'config' is: %s\n", currentValue)
// Clear: 清空整个 Map
fmt.Println("\nClearing the map...")
m.Clear()
// 验证 Map 是否为空
m.Range(func(key, value interface{}) bool {
fmt.Println("This should not be printed.")
return true
})
fmt.Println("Map is empty.")
}

sync.Once 系列函数的演进

Go 1.21.0 引入的 OnceFunc, OnceValue, OnceValues 在后续版本中得到了优化,例如减少了堆内存分配,使其在需要延迟初始化或缓存计算结果的场景下更高效。func

  • OnceFunc(f func()) func():将一个无参数无返回值的函数包装成只执行一次的函数。
  • func OnceValue[T any](f func() T) func() T: 将一个无参数但有单个返回值的函数包装成只执行一次的函数,后续调用返回缓存的结果。
  • func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2): 将一个无参数但有两个返回值的函数包装成只执行一次的函数,后续调用返回缓存的结果。

代码示例 (OnceValue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import (
"fmt"
"sync"
)
func main() {
once := sync.OnceValue(func() int {
sum := 0
for i := 0; i < 1000; i++ {
sum += i
}
fmt.Println("Computed once:", sum)
return sum
})
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
const want = 499500
got := once()
if got != want {
fmt.Println("want", want, "got", got)
}
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}

2. 性能优化与内部实现重构

sync 包的性能至关重要。近期的更新在多个方面对其进行了优化。

sync.Map 的新实现

sync.Map 的内部实现经历了一次重大变更,引入了基于哈希分片三角树(HashTrieMap)的新设计 (Go 1.24)。但这类探索表明了社区在持续寻求提升 sync.Map 在不同并发场景下性能的努力。

sync.Mutex 内部重构

Mutex 在Go1.24.0增加了一个基于HashTrieMap的实现。在Go1.26.0中应该会移除旧的sync.Mutex实现, sync.Map将默认采用HashTrieMap的实现。

https://github.com/golang/go/issues/70683

sync.Once 的原子操作优化

Once.done 字段的实现从 atomic.Uint32 切换到atomic.Bool (Go 1.25)。这不仅提升了代码的可读性和类型安全性,也代表了用现代原子类型替代旧有模式的趋势。

3. 代码正确性与开发者体验

为帮助开发者编写更健壮的并发代码,sync 包在文档和静态分析方面做了大量改进。

noCopy 哨兵的引入

Mutex, RWMutex, WaitGroup, Cond, 和 Map 等核心类型都加入了 noCopy 字段。这是一个特殊的非导出字段,可以被 go vet 工具识别。当开发者无意中复制了这些包含内部状态的同步原语时,go vet 会发出警告,从而在编译前发现潜在的并发 bug。

代码示例 (错误用法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import "sync"
// Counter 是一个带有锁的计数器
type Counter struct {
sync.Mutex
count int
}
// Inc 增加计数器
func (c Counter) Inc() { // 注意:这里使用了值接收器,会导致 Mutex 复制
c.Lock()
c.count++
c.Unlock()
}
func main() {
var c Counter
c.Inc()
}
// 运行 `go vet .` 将会报告:
// main.go:12:6: call of method Inc copies lock value: main.Counter contains sync.Mutex

文档的持续改进

大量的提交都致力于改进和澄清文档,包括:

  • 明确指出 RWMutex 的读锁和写锁不能相互升级或降级。
  • 详细描述了 MapRangeDelete 等方法在并发访问下的行为和内存模型保证。
  • Cond.Wait 的行为提供了更清晰的说明。
  • 在包文档中直接链接到 Go 内存模型,强调其重要性。

4. sync/atomic 的现代化

作为 sync 的底层支撑,sync/atomic 包在 Go 1.19 中引入了基于泛型的类型安全原子类型(如 atomic.Int64, atomic.Pointer[T], atomic.Bool 等)。近两年的变化主要体现在:

  • 鼓励使用新类型:在 sync 包内部,旧的函数式原子操作(如 atomic.LoadUint32)正逐渐被新的类型化方法(如 myAtomicBool.Load())所取代。
  • API 完善:增加了 And/Or 等新的原子位操作函数,并对文档进行了补充,以指导用户从旧 API 过渡到新 API。

总结

过去两年,Go 的 sync 包在保持 API 稳定的同时,向着更易用、更安全、更高性能的方向稳步发展。通过引入 WaitGroup.Go 等便捷的辅助函数,开发者可以编写更简洁的并发代码。noCopy 哨兵和持续完善的文档则提高了代码的健壮性。底层的性能优化和实现重构,确保了 Go 的并发原语能够适应不断增长的性能需求。这些变化共同巩固了 Go 作为一门现代并发语言的地位。